﻿using System;
using System.Collections.Generic;
using System.Data.Objects;
using System.Linq;
using Citi.Scheduler.Domain.Utils;

namespace Citi.Scheduler.Domain
{
    public partial class RoutineSchedule
    {
        public void SendRoutinesToExecutionQueue(bool refreshPredecessor)
        {
            Console.WriteLine("{0} Setting Overwrite Changes", DateTime.Now.ToString("dd/MM/yyy HH:mm:ss"));
            Context.SetOverwriteChanges(Context.Instance.RoutineExecutions);
            Context.SetOverwriteChanges(Context.Instance.Routines);
            Context.SetOverwriteChanges(Context.Instance.RoutineSchedules);
            Context.SetOverwriteChanges(Context.Instance.Schedules);
            Context.SetOverwriteChanges(Context.Instance.RoutineErrors);
            Context.SetOverwriteChanges(Context.Instance.RoutineErrorHistories);
            Context.SetOverwriteChanges(Context.Instance.RoutineExecutionHistories);
            Context.SetOverwriteChanges(Context.Instance.RoutineOnDemands);
            Console.WriteLine("{0} Finished: Set Overwrite Changes", DateTime.Now.ToString("dd/MM/yyy HH:mm:ss"));

            new Routine().FinishExpiredRoutines();
            SendScheduleRoutines();
            SendOnDemandRoutines();
            new RoutineExecution().UpdateStatus(refreshPredecessor);
            UpdateNonHistories();
            InsertNewTentativeErrorRoutine();
            RemoveDuplicates();
        }

        private void InsertNewTentativeErrorRoutine()
        {
            Console.WriteLine("{0} Checking Stopped, Error and Broken in Progress routines", DateTime.Now.ToString("dd/MM/yyy HH:mm:ss"));

            var todayRoutines = new RoutineExecution().SelectAllActive<RoutineExecution>().Where(x => x.ScheduledDateTime.Date == DateTime.Today
                && x.RoutineExecutionHistories.Any());

            Console.WriteLine("{0} {1} Today executions", DateTime.Now.ToString("dd/MM/yyy HH:mm:ss"), todayRoutines.Count());

            var routines = todayRoutines
                           .Where(x =>
                               ((RoutineExecutionStatus)x.RoutineExecutionHistories.Last().Status == RoutineExecutionStatus.Error
                                    || (RoutineExecutionStatus)x.RoutineExecutionHistories.Last().Status == RoutineExecutionStatus.Stopped
                                    || ((RoutineExecutionStatus)x.RoutineExecutionHistories.Last().Status == RoutineExecutionStatus.InProgress && x.IsTimedOut))
                               ).ToList();

            Console.WriteLine("{0} Were found {1} broken routines", DateTime.Now.ToString("dd/MM/yyy HH:mm:ss"), routines.Count());

            foreach (var routine in routines)
            {
                var notExistsAnotherExecution = routine.Tentatives < routine.Routine.TimesToTryAgain
                                                && !todayRoutines.Any(y => (y.ScheduledDateTime > routine.ScheduledDateTime || y.Id > routine.Id)
                                                    && y.RoutineId == routine.RoutineId
                                                    && y.Tentatives >= routine.Tentatives);

                var lastStatus = (RoutineExecutionStatus)routine.LastHistory.Status;

                if (lastStatus == RoutineExecutionStatus.InProgress)
                    RoutineExecutionHistory.InsertHistory(routine.Id, RoutineExecutionStatus.Error, HistoryDetail.UnexpectedError);

                if (notExistsAnotherExecution)
                    RoutineExecution.InsertNewTentative(routine, lastStatus == RoutineExecutionStatus.InProgress ? RoutineExecutionStatus.Error : lastStatus);
            }
        }

        private void RemoveDuplicates()
        {
            Console.WriteLine("{0} Checking duplicates", DateTime.Now.ToString("dd/MM/yyy HH:mm:ss"));

            var duplicates = new RoutineExecution().SelectAllActive<RoutineExecution>()
                .Where(x => x.ScheduledDateTime.Date == DateTime.Today)
                .GroupBy(x => new { x.RoutineId, x.ScheduledDateTime })
                .Where(x => x.Count() > 1).ToList();

            Console.WriteLine("{0} Were found {1} routines duplicated", DateTime.Now.ToString("dd/MM/yyy HH:mm:ss"), duplicates.Count);

            foreach (var item in duplicates)
            {
                var routineExecutions = item.Where(x => !x.RoutineExecutionHistories.Any() || !x.FinishedExecution).ToList();

                if (item.Count() == routineExecutions.Count())
                    routineExecutions.Remove(routineExecutions
                        .FirstOrDefault(x => x.RoutineExecutionHistories.Count == routineExecutions.Max(y => y.RoutineExecutionHistories.Count())));

                foreach (var routineExecutionDuplicated in routineExecutions)
                {
                    foreach (var history in routineExecutionDuplicated.RoutineExecutionHistories.ToList())
                        history.Delete(history);

                    routineExecutionDuplicated.Delete(routineExecutionDuplicated);
                }
            }
        }

        private void UpdateNonHistories()
        {
            Console.WriteLine("{0} Checking for non-histories execution", DateTime.Now.ToString("dd/MM/yyy HH:mm:ss"));
            var routineExecutions = new RoutineExecution().SelectAllActive<RoutineExecution>()
                .Where(x => x.ScheduledDateTime.Date == DateTime.Today && !x.RoutineExecutionHistories.Any()).ToList();

            Console.WriteLine("{1} Were found {0} non-histories execution", routineExecutions.Count(), DateTime.Now.ToString("dd/MM/yyy HH:mm:ss"));

            foreach (var routineExecution in routineExecutions)
            {
                Context.Instance.LoadProperty(routineExecution, x => x.RoutineExecutionHistories, MergeOption.OverwriteChanges);

                if (!routineExecution.RoutineExecutionHistories.Any())
                    RoutineExecutionHistory.InsertHistory(routineExecution.Id, GetStatus(routineExecution.Routine, DateTime.Now, routineExecution.Id), HistoryDetail.InsertedBySystem);
            }

            //foreach (var routineExecution in routineExecutions)
            //    new RoutineExecutionHistory().InsertHistory(routineExecution.Id, GetStatus(routineExecution.Routine, DateTime.Now), HistoryDetail.InsertedBySystem);

        }

        public void SendOnDemandRoutines()
        {
            Console.WriteLine("{0} Checking on demand routine(s) to execution queue", DateTime.Now.ToString("dd/MM/yyy HH:mm:ss"));
            var routinesOnDemand = new RoutineOnDemand().SelectAll<RoutineOnDemand>().Where(x => !x.SentToExecution).ToList();

            Console.WriteLine("{1} Sending {0} on demand routine(s) to execution queue", routinesOnDemand.Count, DateTime.Now.ToString("dd/MM/yyy HH:mm:ss"));

            routinesOnDemand.ForEach(x =>
                {
                    var execution = new RoutineExecution
                        {
                            MachineId = x.Routine.PreferredMachineId,
                            RoutineId = x.Routine.Id,
                            ScheduledDateTime = DateTime.Now,
                            CustomerSLA = x.Routine.CustomerSLA
                        };

                    //Cancel previous executions
                    x.Routine.RoutineExecutions.Where(y => y.ScheduledDateTime.Date == DateTime.Today && !y.ExecutedOrInProgress).ToList()
                       .ForEach(notExecuted => RoutineExecutionHistory.InsertHistory(notExecuted.Id, RoutineExecutionStatus.Canceled, HistoryDetail.CanceledNewExecution));

                    execution.Save(execution);

                    x.SentToExecution = true;
                    x.Save(x);

                    RoutineExecutionHistory.InsertHistory(execution.Id, GetStatus(x.Routine, DateTime.Now, execution.Id), HistoryDetail.InsertedBySystem);
                });
        }

        private void SendScheduleRoutines()
        {
            Console.WriteLine("{0} Checking routines to insert on execution queue", DateTime.Now.ToString("dd/MM/yyy HH:mm:ss"));

            var routinesToRun = new Routine().SelectAllActive<Routine>().Where(x => !x.ScheduleFinished && x.RoutineSchedules.Any()
                                                                    && x.RoutineExecutions.All(y => y.ScheduledDateTime.Date != DateTime.Today))
                                                                    .OrderBy(x => x.RoutineSchedules.Last().Schedule.Time).Distinct().ToList();

            Console.WriteLine("{1} Were found {0} routines to insert on execution queue", routinesToRun.Count, DateTime.Now.ToString("dd/MM/yyy HH:mm:ss"));

            var routines = 0;

            routinesToRun.ForEach(x =>
                {
                    var lastSchedule = x.RoutineSchedules.Last();

                    if (!RunOnDate(lastSchedule.Schedule, DateTime.Today)) return;

                    var scheduledDateTime = DateTime.Today.Add(lastSchedule.Schedule.Time.TimeOfDay);

                    var execution = new RoutineExecution
                        {
                            MachineId = x.PreferredMachineId,
                            RoutineId = x.Id,
                            ScheduledDateTime = scheduledDateTime,
                            CustomerSLA = x.CustomerSLA
                        };

                    execution.Save(execution);

                    RoutineExecutionHistory.InsertHistory(execution.Id, GetStatus(x, scheduledDateTime, execution.Id), HistoryDetail.InsertedBySystem);
                    routines++;
                });

            Console.WriteLine("{1} Sending {0} routine(s) to execution queue", routines, DateTime.Now.ToString("dd/MM/yyy HH:mm:ss"));
        }

        public static bool RunOnDate(Schedule schedule, DateTime date)
        {
            if (date.Date < schedule.StartsOn.Value.Date) return false;

            var actualDayOfWeek = date.DayOfWeek;

            switch ((Periodicity)schedule.Periodicity)
            {
                case Periodicity.BusinessDay:
                    if (!date.IsBusinessDay()) return false;
                    break;
                case Periodicity.MondayWednesdayFriday:
                    if (actualDayOfWeek != DayOfWeek.Monday && actualDayOfWeek != DayOfWeek.Wednesday &&
                        actualDayOfWeek != DayOfWeek.Friday) return false;
                    break;
                case Periodicity.TuesdayThursday:
                    if (actualDayOfWeek != DayOfWeek.Tuesday && actualDayOfWeek != DayOfWeek.Thursday) return false;
                    break;
                case Periodicity.Yearly:
                    if (date.Month == schedule.StartsOn.Value.Month &&
                        date.Day == schedule.StartsOn.Value.Day && date.Year - schedule.StartsOn.Value.Year % schedule.RepeatEvery != 0)
                        return false;
                    break;
                case Periodicity.Weekly:
                    if (schedule.RepeatTypes.All(y => y.Weekday != (int)actualDayOfWeek)) return false;

                    //if ((date - schedule.StartsOn.Value).TotalDays / 7 % schedule.RepeatEvery != 0) return false;
                    if (schedule.StartsOn.Value.DateDifference(DateTimeExtensions.DateInterval.WeekOfYear, date, schedule.StartsOn.Value.DayOfWeek)
                        % schedule.RepeatEvery != 0)
                        return false;

                    break;
                case Periodicity.Monthly:
                    var repeatType = schedule.RepeatTypes.First();

                    if (repeatType.MonthType == (int)MonthType.DayOfWeek && (date.GetWeekOfMonth() != schedule.StartsOn.Value.GetWeekOfMonth()
                            || date.DayOfWeek != schedule.StartsOn.Value.DayOfWeek))
                        return false;

                    if (date.Day != schedule.StartsOn.Value.Day) return false;

                    if (schedule.StartsOn.Value.DateDifference(DateTimeExtensions.DateInterval.Month, date, schedule.StartsOn.Value.DayOfWeek)
                        % schedule.RepeatEvery != 0) return false;

                    break;
                case Periodicity.Daily:
                    if ((date - schedule.StartsOn.Value).TotalDays % schedule.RepeatEvery != 0) return false;
                    break;
            }
            return true;
        } 

        public static RoutineExecutionStatus GetStatus(Routine routine, DateTime scheduledDateTime, int routineExecutionId)
        {
            try
            {
                Context.Instance.LoadProperty(routine, y => y.RoutineExecutions, MergeOption.OverwriteChanges);
                Context.Instance.Refresh(RefreshMode.StoreWins, routine);
                Context.Instance.Refresh(RefreshMode.StoreWins, routine.RoutineExecutions);
            }
            catch { }
            //There are 2 days without routine execution, so it must be canceled
            if ((DateTime.Now - scheduledDateTime).TotalDays >= 2) return RoutineExecutionStatus.CanceledAfterWaiting;

            if (!routine.HasPredecessor)
            {
                if (DateTime.Now >= scheduledDateTime)
                {
                    var status = routine.PreferredMachineId.HasValue ? RoutineExecutionStatus.WaitingSpecificFreeMachine : RoutineExecutionStatus.WaitingFreeMachine;
                    RoutineExecutionHistory.UpdateWaitingExecutionFolder(status, routineExecutionId);
                    return status;
                }
                else
                    return RoutineExecutionStatus.Scheduled;
            }

            try
            {
                Context.Instance.LoadProperty(routine, y => y.PredecessorRoutine, MergeOption.OverwriteChanges);
                Context.Instance.Refresh(RefreshMode.StoreWins, routine.PredecessorRoutine);
                Context.Instance.LoadProperty(routine, y => y.PredecessorRoutine.RoutineExecutions, MergeOption.OverwriteChanges);
                Context.Instance.Refresh(RefreshMode.StoreWins, routine.PredecessorRoutine.RoutineExecutions);
            }
            catch
            {
            }

            if (routine.PredecessorRoutine.RoutineExecutions == null) return RoutineExecutionStatus.PredecessorNotScheduled;

            var lastPredecessor = routine.PredecessorRoutine.RoutineExecutions.LastOrDefault(predecessor => predecessor.ScheduledDateTime.Date == DateTime.Today);

            if (lastPredecessor == null) return RoutineExecutionStatus.PredecessorNotScheduled;

            if (lastPredecessor.Error) return RoutineExecutionStatus.PredecessorRoutineWithError;

            if (lastPredecessor.ExecutedWithoutError)
            {
                if (DateTime.Now >= scheduledDateTime)
                    return routine.PreferredMachineId.HasValue ? RoutineExecutionStatus.WaitingSpecificFreeMachine : RoutineExecutionStatus.WaitingFreeMachine;
                else
                    return RoutineExecutionStatus.Scheduled;
            }

            return lastPredecessor.FinishedExecution
                       ? DateTime.Now >= scheduledDateTime
                            ? routine.PreferredMachineId.HasValue ? RoutineExecutionStatus.WaitingSpecificFreeMachine : RoutineExecutionStatus.WaitingFreeMachine
                            : RoutineExecutionStatus.Scheduled
                       : RoutineExecutionStatus.WaitingPredecessorRoutine;
        }

        public IEnumerable<Routine> GetExecutionScheduled(DateTime date, int routineId = 0, int categoryId = 0)
        {
            var routines = categoryId != 0 ? GetAllChildrenRoutines(categoryId) : SelectAllActive<Routine>();

            return routines.Where(x => x.RoutineSchedules.Any()
                && (x.Id == routineId || routineId == 0) && x.RoutineSchedules.Last().Date <= date.AddMonths(1).AddSeconds(-10)).ToList();
        }

        public IEnumerable<Routine> GetAllChildrenRoutines(int categoryId)
        {
            var routines = SelectAllActive<Routine>().Where(x => x.CategoryId == categoryId).ToList();

            if (!routines.Any()) return new List<Routine>();

            foreach (var child in routines.First().Category.Children)
                routines.AddRange(GetAllChildrenRoutines(child.Id));

            return routines;
        }
    }
}
